/**
 * FIFS - The Find It File System
 * Copyright (C) 2006 Chris Oklota (chris at oklota dot net)
 * 
 * This program is distributed under the terms of the GNU LGPL.
 * See the file http://www.gnu.org/licenses/lgpl.txt
 */
package fifs;


import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;

import org.apache.log4j.PropertyConfigurator;
import org.apache.log4j.Logger;

import fuse.Filesystem;
import fuse.FuseDirEnt;
import fuse.FuseException;
import fuse.FuseFtype;
import fuse.FuseMount;
import fuse.FuseStat;
import fuse.FuseStatfs;

/**
 * The FUSE-J implementation class for the Find It File System
 */
public class Fifs implements Filesystem {

    /** The logger for this class */
    public static final Logger log = Logger.getLogger(Fifs.class);

    /** The configuration file for the filesystem */
    public static final String CONFIG_FILE = "/etc/fifs.conf";

    /** The file system block size. Only used for returning stats. */
    public static final int FIFS_BLOCK_SIZE = 1024;
    
    /** Arbitrary number of blocks supported by the filesystem, roughly 50GB */
    public static final int FIFS_BLOCKS = 52428800;
    
    /** The maximum filesystem filename size */
    public static final int FIFS_MAX_FILENAME_SIZE = 255;
    
    /** The parsed properties from the configuration file */
    private Properties properties;

    /** The data access object for the filesystem's backing store */
    private FifsDao dao;

    /** The cached set of all tags currently defined in the DB */
    private Set<String> tagCache = new HashSet<String>();
    
    /** The cached set of open files (file attributes only, no content) */
    private HashMap<Long, FifsFile> openFiles = new HashMap<Long, FifsFile>();
    
    /** The mount point of this filesystem instance */
    private String mountPoint;
    
    
    /**
     * Properties setter
     */
    public void setProperties(Properties properties) {
        this.properties = properties;
    }
    
    /**
     * Properties getter
     */
    public Properties getProperties() {
        return this.properties;
    }
    
    /**
     * Dao setter
     */
    public void setDao(FifsDao dao) {
        this.dao = dao;
    }

    /**
     * Dao getter
     */
    public FifsDao getDao() {
        return dao;
    }
    
    /**
     * Mount point setter
     */
    public void setMountPoint(String mountPoint) {
        this.mountPoint = mountPoint;
    }
    
    /**
     * Helper method to add a set of tags to the dao and the cache
     * @param tags
     * @throws FuseException
     */
    public synchronized void addTags(Set<String> tags) throws FuseException {
        if (tagCache.size() == 0) {
            Set<String> allTags = dao.getAllTags();
            if (allTags != null) {
                tagCache.addAll(allTags);
            }
        }
        for (String tag : tags) {
            if (!tagCache.contains(tag)) {
                if (dao.createTag(tag)) {
                    tagCache.add(tag);
                } else {
                    FuseException fe = new FuseException("Error creating tag").initErrno(FuseException.EIO);
                    if (log.isDebugEnabled()) {
                        log.debug("Error creating tag", fe);
                    }
                    throw fe;
                }
            }
        }   
    }
    
    // -------------------- Filesystem Interface Methods ---------------------//

    /**
     * Get the attributes for the given path, which may be a directory or file.
     */
    public synchronized FuseStat getattr(String path) throws FuseException {
        
        if (log.isTraceEnabled()) {
            log.trace("getattr - path: " + path);
        }
        
        Set<String> tags = FifsUtils.parseTags(path);
        String filename = FifsUtils.parseFilename(path);
        FifsFile file = null;
        
        // if a fh was provided, use that, but it better match
        long fh = FifsUtils.parseFileHandle(path);
        if (fh != -1) {
            file = dao.getFile(fh);
            if ((file == null) || 
                !(file.getFilename().equals(filename)) || 
                !(file.getTags().containsAll(tags))) {
                FuseException fe = new FuseException("No Such Entry").initErrno(FuseException.ENOENT);
                if (log.isDebugEnabled()) {
                    log.debug("No Such Entry", fe);
                }
                throw fe;
            }
        } else {
            file = dao.getFile(tags, filename);
        }
        
        FuseStat stat = null;
        if (file != null) {
        	stat = file.getAttributes();
        }
        
        if (stat == null) {
        
        	// file doesn't exist, let's check if this is a directory (meaning that
        	// each path element must be a tag (or eventually a search entry)
        	tags = FifsUtils.parseDirTags(path);
        	
        	synchronized (this) {
                if (tagCache.size() == 0) {
                	Set<String> allTags = dao.getAllTags();
                	if (allTags != null) {
                		tagCache.addAll(allTags);
                	}
                }
                if (path.equals("/") || tagCache.containsAll(tags)) {
                    stat = new FuseStat();
                    stat.mode = FuseFtype.TYPE_DIR | 0755;
                    stat.nlink = 1;
                    stat.uid = 0;
                    stat.gid = 0;
                    stat.size = 4096;
                    stat.atime = stat.mtime = stat.ctime = 0;
                    stat.blocks = 4;
                } else {
                    FuseException fe = new FuseException("No Such Entry II").initErrno(FuseException.ENOENT);
                    if (log.isDebugEnabled()) {
                        log.debug("No Such Entry II", fe);
                    }
                    throw fe;
                }
            }
        }
        return stat;
    }

    /**
     * Return an array of FuseDirEnt objects for all entries (subdirectories or 
     * files) in the given directory path.
     * 
     * @param path The absolute path of a directory
     */
    public synchronized FuseDirEnt[] getdir(String path) throws FuseException {
        
        if (log.isTraceEnabled()) {
            log.trace("getdir - path: " + path);
        }
        
        ArrayList<FuseDirEnt> entries = new ArrayList<FuseDirEnt>();
        FuseDirEnt ent = null;
        Set<String> subdirTags = new HashSet<String>();
        HashMap<String, FuseDirEnt> seenFilenames = new HashMap<String, FuseDirEnt>();
        Set<String> ambiguousFilenames = new HashSet<String>();
        
        // get the tags from the directory path to search on
        Set<String> tags = FifsUtils.parseDirTags(path);
        
        // get all files with these tags and add them to the directory listing
        Set<FifsFile> files = dao.getAllFilesByTags(tags);
        if (files != null) {
	        for (FifsFile f : files) {
                subdirTags.addAll(f.getTags());
	            ent = new FuseDirEnt();
                ent.inode = (int)f.getFh();
	            ent.mode = FuseFtype.TYPE_FILE;
	            String name = f.getFilename();
                
                // if we've already seen this name in the listing, tack on fh
                if (seenFilenames.containsKey(name)) {
                    ent.name = name + ':' + f.getFh();
                    ambiguousFilenames.add(name);
                } else {
                    ent.name = name;
                    seenFilenames.put(name, ent);
                }
                
	            entries.add(ent);
	        }
            
            // for every ambiguous name, go back and add the fh to the name
            for (String name : ambiguousFilenames) {
                FuseDirEnt entry = seenFilenames.get(name);
                entry.name = entry.name + ':' + entry.inode;
            }
        }
        
        // ensure thee tag cache is loaded
        synchronized (this) {
            if (tagCache.size() == 0) {
            	Set<String> allTags = dao.getAllTags();
            	if (allTags != null) {
            		tagCache.addAll(allTags);
            	}
            }
        }

        // if it's the root dir, add all tags to the listing
        if (path.equals("/")) {
            subdirTags.addAll(tagCache);
        }

        // remove subdir tags that are already in the path
        subdirTags.removeAll(tags);
        
        // create subdir entries
        for (String tag : subdirTags) {
            ent = new FuseDirEnt();
            ent.mode = FuseFtype.TYPE_DIR;
            ent.name = tag;
            entries.add(ent);
        }
        
        FuseDirEnt[] entryArray = new FuseDirEnt[entries.size()];
        for (int i = 0; i < entries.size(); i++) {
        	entryArray[i] = entries.get(i);
        }
        return entryArray;
    }

    /**
     * This method will be called for the creation of all non-directory and non-symlink
     * files.
     * 
     * @param path The absolute path to create a file
     * @param mode The file creation mode
     * @param rdev ?
     */
    public synchronized void mknod(String path, int mode, int rdev) throws FuseException {
        
        if (log.isTraceEnabled()) {
            log.trace("mknod - path: " + path + ", mode: " + mode + ", rdev: "
                    + rdev);
        }
        
        Set<String> tags = FifsUtils.parseTags(path);
        String filename = FifsUtils.parseFilename(path);
        
        // check the filename length
        if (filename.length() > FIFS_MAX_FILENAME_SIZE) {
        	FuseException fe = new FuseException("Filename too long").initErrno(FuseException.ENAMETOOLONG);
            if (log.isDebugEnabled()) {
                log.debug("Filename too long", fe);
            }
            throw fe;
        }
        
        // check that the filename does not contain any ":" colons
        if (filename.indexOf(':') != -1) {
            FuseException fe = new FuseException("Filenames cannot contain colons in FIFS").initErrno(FuseException.EINVAL);
            if (log.isDebugEnabled()) {
                log.debug("Filenames cannot contain colons in FIFS", fe);
            }
            throw fe;
        }
        
        // get any implicit tags
        Set<String> implicitTags = FifsUtils.getImplicitTags(properties, filename);
        tags.addAll(implicitTags);
        
        // add any new directories to the database and the local tag cache
        synchronized (this) {
            if (tagCache.size() == 0) {
                Set<String> allTags = dao.getAllTags();
                if (allTags != null) {
                    tagCache.addAll(allTags);
                }
            }
            for (String tag : implicitTags) {
                if (!tagCache.contains(tag)) {
                    if (dao.createTag(tag)) {
                        tagCache.add(tag);
                    } else {
                        FuseException fe = new FuseException("Error creating tag").initErrno(FuseException.EIO);
                        if (log.isDebugEnabled()) {
                            log.debug("Error creating tag", fe);
                        }
                        throw fe;
                    }
                }
            }   
        }
        
        FifsFile newFile = new FifsFile();
        newFile.setFilename(filename);
        newFile.setTags(tags);
        FuseStat stat = new FuseStat();
        stat.mode = FuseFtype.TYPE_FILE | mode;
        stat.atime = stat.mtime = stat.ctime = (int)(System.currentTimeMillis() / 1000L);
        newFile.setAttributes(stat);
        
        // create the file in the database
        if (!dao.createFile(newFile)) {
        	FuseException fe = new FuseException("Error creating file").initErrno(FuseException.EIO);
            if (log.isDebugEnabled()) {
                log.debug("Error creating file", fe);
            }
            throw fe;
        }
    }

    /**
     * This method is called to create a directory
     * 
     * @param path The list of tags to create in the database
     * @param mode The mode of the new directory, we ignore it
     */
    public synchronized void mkdir(String path, int mode) throws FuseException {
        
        if (log.isTraceEnabled()) {
            log.trace("mkdir - path: " + path + ", mode: " + mode);
        }
       
        // get the tags from the directory path to search on
        Set<String> tags = FifsUtils.parseDirTags(path);
        
        // can't create root directory
        if (tags == null || tags.size() == 0) {
            FuseException fe = new FuseException("File exists").initErrno(FuseException.EEXIST);
            if (log.isDebugEnabled()) {
                log.debug("File exists", fe);
            }
            throw fe;
        }
        
        // check that the path does not contain any ":" colons
        if (path.indexOf(':') != -1) {
            FuseException fe = new FuseException("Directory paths cannot contain colons in FIFS").initErrno(FuseException.EINVAL);
            if (log.isDebugEnabled()) {
                log.debug("Directory paths cannot contain colons in FIFS", fe);
            }
            throw fe;
        }
        
        // add the new directory to the database and the local tag cache
        synchronized (this) {
            for (String tag : tags) {
                if (!tagCache.contains(tag)) {
                    if (dao.createTag(tag)) {
                        tagCache.add(tag);
                    } else {
                        FuseException fe = new FuseException("Error creating tag").initErrno(FuseException.EIO);
                        if (log.isDebugEnabled()) {
                            log.debug("Error creating tag", fe);
                        }
                        throw fe;
                    }
                }
            }   
        }
    }

    /**
     * Delete the given file.
     * 
     * @param path The absolute path of the file delete
     */
    public synchronized void unlink(String path) throws FuseException {
        
        if (log.isTraceEnabled()) {
            log.trace("unlink - path: " + path);
        }
        
        Set<String> tags = FifsUtils.parseTags(path);
        String filename = FifsUtils.parseFilename(path);
        FifsFile file = null;
        // if a fh was provided, use that, but it better match
        long fh = FifsUtils.parseFileHandle(path);
        if (fh != -1) {
            file = dao.getFile(fh);
            if ((file == null) || 
                !(file.getFilename().equals(filename)) || 
                !(file.getTags().containsAll(tags))) {
                FuseException fe = new FuseException("No Such Entry").initErrno(FuseException.ENOENT);
                if (log.isDebugEnabled()) {
                    log.debug("No such entry", fe);
                }
                throw fe;
            }
        } else {
            file = dao.getFile(tags, filename);
        }
        
        if (file == null) {
            FuseException fe = new FuseException("No Such Entry").initErrno(FuseException.ENOENT);
            if (log.isDebugEnabled()) {
                log.debug("No such entry", fe);
            }
            throw fe;
        }
        if (!dao.deleteFile(file)) {
            FuseException fe = new FuseException("Error deleting file").initErrno(FuseException.EIO);
            if (log.isDebugEnabled()) {
                log.debug("Error deleting file", fe);
            }
            throw fe;
        }
    }

    /**
     * Delete the given directory(tag) from the database.
     * Parse the path and delete the last directory(tag) from the database. Get
     * the list of files tagged with that tag, and remove the tag from each of
     * them. If removing the tag makes the file non-unique, delete the file as
     * well.
     * 
     * @param path The absolute directory path to delete
     */
    public synchronized void rmdir(String path) throws FuseException {
        
        if (log.isTraceEnabled()) {
            log.trace("rmdir - path: " + path);
        }
        
        String lastDir = FifsUtils.parseFilename(path);
        if ((lastDir == null) || (lastDir.length() == 0)) {
            FuseException fe = new FuseException("Cannot delete root").initErrno(FuseException.EPERM);
            if (log.isDebugEnabled()) {
                log.debug("Cannot delete root", fe);
            }
            throw fe;
        }
        HashSet<String> tags = new HashSet<String>();
        tags.add(lastDir);
        Set<FifsFile> files = dao.getAllFilesByTags(tags);
        for (FifsFile f : files) {
            f.getTags().remove(lastDir);
            if (!dao.updateFileTags(f)) {
                FuseException fe = new FuseException("Error updating file tags").initErrno(FuseException.EIO);
                if (log.isDebugEnabled()) {
                    log.debug("Error updating file tags", fe);
                }
                throw fe;
            }
        }
        if (!dao.deleteTag(lastDir)) {
            FuseException fe = new FuseException("Error deleting tag").initErrno(FuseException.EIO);
            if (log.isDebugEnabled()) {
                log.debug("Error deleting tag", fe);
            }
            throw fe;
        }
        tagCache.remove(lastDir);
    }

    /**
     * Rename the given path. Both paths can refer to a file or directory.
     * @param from A file or directory source
     * @param to A file or directory destination
     */
    public synchronized void rename(String from, String to) throws FuseException {
        
        if (log.isTraceEnabled()) {
            log.trace("rename - from: " + from + ", to: " + to);
        }
        
        boolean fromIsFile = false;
        boolean fromIsDir = false;
        boolean toIsFile = false;
        boolean toIsDir = false;
        
        // ensure the tag cache has been loaded
        synchronized (this) {
            if (tagCache.size() == 0) {
                Set<String> allTags = dao.getAllTags();
                if (allTags != null) {
                    tagCache.addAll(allTags);
                }
            }
        }
        
        // determine if "from" is a file or directory
        Set<String> fromTags = FifsUtils.parseTags(from);
        String fromFilename = FifsUtils.parseFilename(from);
        FifsFile fromFile = null;
        // if a fh was provided, use that, but it better match
        long fromFh = FifsUtils.parseFileHandle(from);
        if (fromFh != -1) {
            fromFile = dao.getFile(fromFh);
            if ((fromFile == null) || 
                !(fromFile.getFilename().equals(fromFilename)) || 
                !(fromFile.getTags().containsAll(fromTags))) {
                FuseException fe = new FuseException("No Such Entry").initErrno(FuseException.ENOENT);
                if (log.isDebugEnabled()) {
                    log.debug("No such entry", fe);
                }
                throw fe;
            }
        } else {
            fromFile = dao.getFile(fromTags, fromFilename);
        }
        if (fromFile == null) {
            // from is not a file, check for directory
            fromTags = FifsUtils.parseDirTags(from);
            if (from.equals("/") || tagCache.containsAll(fromTags)) {
                fromIsDir = true;
            } else {
                FuseException fe = new FuseException("No Such Entry").initErrno(FuseException.ENOENT);
                if (log.isDebugEnabled()) {
                    log.debug("No such entry", fe);
                }
                throw fe;
            }
        } else {
            fromIsFile = true;
        }
        
        // determine if "to" is a file or directory
        Set<String> toTags = FifsUtils.parseTags(to);
        String toFilename = FifsUtils.parseFilename(to);
        FifsFile toFile = null;
        // if a fh was provided, use that, but it better match
        long toFh = FifsUtils.parseFileHandle(to);
        if (toFh != -1) {
            toFile = dao.getFile(toFh);
            if ((toFile == null) || 
                !(toFile.getFilename().equals(toFilename)) || 
                !(toFile.getTags().containsAll(toTags))) {
                FuseException fe = new FuseException("No Such Entry").initErrno(FuseException.ENOENT);
                if (log.isDebugEnabled()) {
                    log.debug("No such entry", fe);
                }
                throw fe;
            }
        } else {
            toFile = dao.getFile(toTags, toFilename);
        }
        if (toFile == null) {
            // to is not an existing file, check for directory
            toTags = FifsUtils.parseDirTags(to);
            if (to.equals("/") || tagCache.containsAll(toTags)) {
                toIsDir = true;
            } else {
                // not a directory either 
                // if from is a file, will assume to refers to a new file then, replace toTags
                if (fromIsFile) {
                    toTags = FifsUtils.parseTags(to);
                    // directory moving the new file to must exist however
                    if (!tagCache.containsAll(toTags)) {
                        FuseException fe = new FuseException("No Such Entry").initErrno(FuseException.ENOENT);
                        if (log.isDebugEnabled()) {
                            log.debug("No such entry", fe);
                        }
                        throw fe;
                    }
                } else {
                    // from is a directory, so we will assume that we are moving all files in it
                    // to another directory, make sure that it exists
                    toIsDir = true;
                    toTags = FifsUtils.parseTags(to);
                    // parent directory(s) of the new directory must exist however
                    if (!tagCache.containsAll(toTags)) {
                        FuseException fe = new FuseException("No Such Entry").initErrno(FuseException.ENOENT);
                        if (log.isDebugEnabled()) {
                            log.debug("No such entry", fe);
                        }
                        throw fe;
                    }
                    // create the new directory, and add it to the destination toTags set
                    if (toFilename != null) {
                        if (!dao.createTag(toFilename)) {
                            FuseException fe = new FuseException("I/O Error").initErrno(FuseException.EIO);
                            if (log.isDebugEnabled()) {
                                log.debug("I/O Error", fe);
                            }
                            throw fe;
                        } else {
                            toTags.add(toFilename);
                            tagCache.add(toFilename);
                        }
                    }
                }
            }
        } else {
            toIsFile = true;
        }
        
        if (fromIsFile) {
            // renaming a single file
            // if destination is not directory, we need to rename filename
            if (!toIsDir) {
                // get any implicit tags
                Set<String> implicitTags = FifsUtils.getImplicitTags(properties, toFilename);
                toTags.addAll(implicitTags);
                // add any new directories to the database and the local tag cache
                synchronized (this) {
                    for (String tag : implicitTags) {
                        if (!tagCache.contains(tag)) {
                            if (dao.createTag(tag)) {
                                tagCache.add(tag);
                            } else {
                                FuseException fe = new FuseException("Error creating tag").initErrno(FuseException.EIO);
                                if (log.isDebugEnabled()) {
                                    log.debug("Error creating tag", fe);
                                }
                                throw fe;
                            }
                        }
                    }   
                }
                fromFile.setFilename(toFilename);
                fromFile.getAttributes().atime = fromFile.getAttributes().mtime = (int)(System.currentTimeMillis()/1000L);
                if (!dao.updateFileAttributes(fromFile)) {
                    FuseException fe = new FuseException("I/O Error").initErrno(FuseException.EIO);
                    if (log.isDebugEnabled()) {
                        log.debug("I/O Error", fe);
                    }
                    throw fe;
                }
            }
            // remove all tags in the from path from the file, and add all tags in the to path
            fromFile.getTags().removeAll(fromTags);
            fromFile.getTags().addAll(toTags);
            if (!dao.updateFileTags(fromFile)) {
                FuseException fe = new FuseException("I/O Error").initErrno(FuseException.EIO);
                if (log.isDebugEnabled()) {
                    log.debug("I/O Error", fe);
                }
                throw fe;
            }
        } else {
            // renaming all files in a directory
            // destination must be a directory
            if (!toIsDir) {
                FuseException fe = new FuseException("No Such Entry").initErrno(FuseException.ENOENT);
                if (log.isDebugEnabled()) {
                    log.debug("I/O Error", fe);
                }
                throw fe;
            }
            // remove all tags in the from path from each file, and add all tags in the to path
            Set<FifsFile> files = dao.getAllFilesByTags(fromTags);
            for (FifsFile f : files) {
                f.getTags().removeAll(fromTags);
                f.getTags().addAll(toTags);
                f.getAttributes().atime = f.getAttributes().mtime = (int)(System.currentTimeMillis()/1000L);
                if (!dao.updateFileTags(f) || !dao.updateFileAttributes(f)) {
                    FuseException fe = new FuseException("I/O Error").initErrno(FuseException.EIO);
                    if (log.isDebugEnabled()) {
                        log.debug("I/O Error", fe);
                    }
                    throw fe;
                }
            }
        }
    }

    /**
     * Update the mode bits on the given path
     * @param path The file to the file or directory to change modes
     * @param mode The new mode bits for the file
     */
    public synchronized void chmod(String path, int mode) throws FuseException {
        
        if (log.isTraceEnabled()) {
            log.trace("chmod - path: " + path + ", mode: " + mode);
        }
        
        Set<String> tags = FifsUtils.parseTags(path);
        String filename = FifsUtils.parseFilename(path);
        FifsFile file = null;
        // if a fh was provided, use that, but it better match
        long fh = FifsUtils.parseFileHandle(path);
        if (fh != -1) {
            file = dao.getFile(fh);
            if ((file == null) || 
                !(file.getFilename().equals(filename)) || 
                !(file.getTags().containsAll(tags))) {
                FuseException fe = new FuseException("No Such Entry").initErrno(FuseException.ENOENT);
                if (log.isDebugEnabled()) {
                    log.debug("No such entry", fe);
                }
                throw fe;
            }
        } else {
            file = dao.getFile(tags, filename);
        }
        if (file == null) {
        	
        	// file doesn't exist, let's check if this is a directory (meaning that
        	// each path element must be a tag)
        	tags = FifsUtils.parseDirTags(path);
        	
        	synchronized (this) {
        		if (tagCache.size() == 0) {
        			Set<String> allTags = dao.getAllTags();
                	if (allTags != null) {
                		tagCache.addAll(allTags);
                	}
                }
                if (path.equals("/") || tagCache.containsAll(tags)) {
                    // can't change directory modes, do nothing...
                } else {
                	// neither a valid directory or existing filename, throw error
                    FuseException fe = new FuseException("No Such Entry").initErrno(FuseException.ENOENT);
                    if (log.isDebugEnabled()) {
                        log.debug("No such entry", fe);
                    }
                    throw fe;
                }
            }
        } else {
        	// update the file attributes
        	file.getAttributes().mode = FuseFtype.TYPE_FILE | mode;
            file.getAttributes().atime = file.getAttributes().mtime = (int)(System.currentTimeMillis() / 1000L);
            if (!dao.updateFileAttributes(file)) {
            	FuseException fe = new FuseException("Error creating file").initErrno(FuseException.EIO);
                if (log.isDebugEnabled()) {
                    log.debug("Error creating file", fe);
                }
                throw fe;
            }
        }
    }

    /**
     * Change the given file owner
     * @param path The file to the file or directory to change owners
     * @param uid The new owner ID of the file
     * @param gid The new group ID of the file
     */
    public synchronized void chown(String path, int uid, int gid) throws FuseException {
        
        if (log.isTraceEnabled()) {
            log.trace("chown - path: " + path + ", uid: " + uid + ", gid: "
                    + gid);
        }
        
        Set<String> tags = FifsUtils.parseTags(path);
        String filename = FifsUtils.parseFilename(path);
        FifsFile file = null;
        // if a fh was provided, use that, but it better match
        long fh = FifsUtils.parseFileHandle(path);
        if (fh != -1) {
            file = dao.getFile(fh);
            if ((file == null) || 
                !(file.getFilename().equals(filename)) || 
                !(file.getTags().containsAll(tags))) {
                FuseException fe = new FuseException("No Such Entry").initErrno(FuseException.ENOENT);
                if (log.isDebugEnabled()) {
                    log.debug("No such entry", fe);
                }
                throw fe;
            }
        } else {
            file = dao.getFile(tags, filename);
        }
        if (file == null) {
        	
        	// file doesn't exist, let's check if this is a directory (meaning that
        	// each path element must be a tag (or eventually a search entry)
        	tags = FifsUtils.parseDirTags(path);
        	
        	synchronized (this) {
        		if (tagCache.size() == 0) {
        			Set<String> allTags = dao.getAllTags();
                	if (allTags != null) {
                		tagCache.addAll(allTags);
                	}
                }
                if (path.equals("/") || tagCache.containsAll(tags)) {
                    // can't change directory owners, do nothing...
                } else {
                	// neither a valid directory or existing filename, throw error
                    FuseException fe = new FuseException("No Such Entry").initErrno(FuseException.ENOENT);
                    if (log.isDebugEnabled()) {
                        log.debug("No such entry", fe);
                    }
                    throw fe;
                }
            }
        } else {
        	// update the file attributes
        	file.getAttributes().uid = uid;
        	file.getAttributes().gid = gid;
            file.getAttributes().atime = file.getAttributes().mtime = (int)(System.currentTimeMillis() / 1000L);
            if (!dao.updateFileAttributes(file)) {
            	FuseException fe = new FuseException("Error creating file").initErrno(FuseException.EIO);
                if (log.isDebugEnabled()) {
                    log.debug("Error creating file", fe);
                }
                throw fe;
            }
        }
    }

    /**
     * Truncate the given file to the given length
     * @param path The absolute file path to truncate
     * @param size The new file size
     */
    public synchronized void truncate(String path, long size) throws FuseException {
        
        if (log.isTraceEnabled()) {
            log.trace("truncate - path: " + path + ", size: " + size);
        }
        
        Set<String> tags = FifsUtils.parseTags(path);
        String filename = FifsUtils.parseFilename(path);
        FifsFile file = null;
        // if a fh was provided, use that, but it better match
        long fh = FifsUtils.parseFileHandle(path);
        if (fh != -1) {
            file = dao.getFile(fh);
            if ((file == null) || 
                !(file.getFilename().equals(filename)) || 
                !(file.getTags().containsAll(tags))) {
                FuseException fe = new FuseException("No Such Entry").initErrno(FuseException.ENOENT);
                if (log.isDebugEnabled()) {
                    log.debug("No such entry", fe);
                }
                throw fe;
            }
        } else {
            file = dao.getFile(tags, filename);
        }
        if (file == null) {
            FuseException fe = new FuseException("No Such Entry").initErrno(FuseException.ENOENT);
            if (log.isDebugEnabled()) {
                log.debug("No such entry", fe);
            }
            throw fe;
        }
        
        if (!dao.setFileSize(file.getFh(), size)) {
            FuseException fe = new FuseException("I/O Error").initErrno(FuseException.EIO);
            if (log.isDebugEnabled()) {
                log.debug("I/O Error", fe);
            }
            throw fe;
        } else {
            file.getAttributes().size = size;
            file.getAttributes().blocks = (int) (size + FIFS_BLOCK_SIZE - 1) / FIFS_BLOCK_SIZE;
            file.getAttributes().atime = file.getAttributes().mtime = (int)(System.currentTimeMillis()/1000L);
            if (!dao.updateFileAttributes(file)) {
                FuseException fe = new FuseException("I/O Error").initErrno(FuseException.EIO);
                if (log.isDebugEnabled()) {
                    log.debug("I/O Error", fe);
                }
                throw fe;
            }
        }
    }

    /**
     * Update the given file's access and modified times.
     * @param path The absolute path to the file or directory to update
     * @param atime The file access time
     * @param mtime The file modification time
     */
    public synchronized void utime(String path, int atime, int mtime) throws FuseException {
        
        if (log.isTraceEnabled()) {
            log.trace("utime - path: " + path + ", atime: " + atime
                    + ", mtime: " + mtime);
        }
        
        Set<String> tags = FifsUtils.parseTags(path);
        String filename = FifsUtils.parseFilename(path);
        FifsFile file = null;
        // if a fh was provided, use that, but it better match
        long fh = FifsUtils.parseFileHandle(path);
        if (fh != -1) {
            file = dao.getFile(fh);
            if ((file == null) || 
                !(file.getFilename().equals(filename)) || 
                !(file.getTags().containsAll(tags))) {
                FuseException fe = new FuseException("No Such Entry").initErrno(FuseException.ENOENT);
                if (log.isDebugEnabled()) {
                    log.debug("No such entry", fe);
                }
                throw fe;
            }
        } else {
            file = dao.getFile(tags, filename);
        }
        if (file == null) {
        	
        	// file doesn't exist, let's check if this is a directory (meaning that
        	// each path element must be a tag (or eventually a search entry)
        	tags = FifsUtils.parseDirTags(path);
        	
        	synchronized (this) {
        		if (tagCache.size() == 0) {
                    Set<String> allTags = dao.getAllTags();
                	if (allTags != null) {
                		tagCache.addAll(allTags);
                	}
                }
                if (path.equals("/") || tagCache.containsAll(tags)) {
                    // can't change directory times, do nothing...
                } else {
                	// neither a valid directory or existing filename, throw error
                    FuseException fe = new FuseException("No Such Entry").initErrno(FuseException.ENOENT);
                    if (log.isDebugEnabled()) {
                        log.debug("No such entry", fe);
                    }
                    throw fe;
                }
            }
        } else {
        	// update the file attributes
        	file.getAttributes().atime = atime;
        	file.getAttributes().mtime = mtime;
            if (!dao.updateFileAttributes(file)) {
                FuseException fe = new FuseException("I/O Error").initErrno(FuseException.EIO);
                if (log.isDebugEnabled()) {
                    log.debug("I/O Error", fe);
                }
                throw fe;
            }
        }
    }

    /**
     * Return stats about the filesystem as a whole. Any fields that are unknown
     * can simply be set to 0.
     */
    public synchronized FuseStatfs statfs() throws FuseException {
        
        if (log.isTraceEnabled()) {
            log.trace("statfs");
        }
        
        FuseStatfs stat = new FuseStatfs();
        stat.blocks = FIFS_BLOCKS;
        stat.blocksFree = FIFS_BLOCKS - 1024;
        stat.blocksAvail = FIFS_BLOCKS - 1024;
        stat.blockSize = FIFS_BLOCK_SIZE;
        stat.files = 0;
        stat.filesFree = 0;
        stat.namelen = FIFS_MAX_FILENAME_SIZE;
        return stat;
    }

    /**
     * Open the given file and return a long file handle. For FIFS, all files
     * should have a unique long ID in the database file table that can be returned.
     * 
     * @param path The absolute path to a file to open
     * @param flags the flags to use when opening the file (create, readonly, append, etc)
     */
    public synchronized long open(String path, int flags) throws FuseException {
        
        if (log.isTraceEnabled()) {
            log.trace("open - path: " + path + ", flags: " + flags);
        }
        
        // TODO - how to handle file open flags?
        // TODO - how to handle user permissions?
        // TODO - how to handle search queries in the file?
        
        Set<String> tags = FifsUtils.parseTags(path);
        String filename = FifsUtils.parseFilename(path);
        FifsFile file = null;
        // if a fh was provided, use that, but it better match
        long fh = FifsUtils.parseFileHandle(path);
        if (fh != -1) {
            file = dao.getFile(fh);
            if ((file == null) || 
                !(file.getFilename().equals(filename)) || 
                !(file.getTags().containsAll(tags))) {
                FuseException fe = new FuseException("No Such Entry").initErrno(FuseException.ENOENT);
                if (log.isDebugEnabled()) {
                    log.debug("No such entry", fe);
                }
                throw fe;
            }
        } else {
            file = dao.getFile(tags, filename);
        }
        if (file == null) {
            FuseException fe = new FuseException("No Such Entry").initErrno(FuseException.ENOENT);
            if (log.isDebugEnabled()) {
                log.debug("No such entry", fe);
            }
            throw fe;
        }
        
        openFiles.put(file.getFh(), file);
        return file.getFh();
    }

    /**
     * This is called when the last filehandle is closed (?) Cleanup the local 
     * resources for the given file here.
     */
    public synchronized void release(String path, long fh, int flags) throws FuseException {
        if (log.isTraceEnabled()) {
            log.trace("release - path: " + path + ", fh: " + fh + ", flags: "
                    + flags);
        }
        
        // the file should be open
        if (!openFiles.containsKey(fh)) {
            FuseException fe = new FuseException("Bad file handle").initErrno(FuseException.EBADF);
            if (log.isDebugEnabled()) {
                log.debug("Bad file handle", fe);
            }
            throw fe;
        }
        
        // remove the file from the open files list
        FifsFile file = openFiles.remove(fh);
        
        // run the plugin if the file has changed since it was opened
        if (file.isDirty()) {
            file.setDirty(false);
            FifsUtils.runPlugin(this, file, mountPoint + path);
        }
    }
    
    /**
     * Read the file contents from the given offset.
     * 
     * Initially this request can be satisfied from local memory, so no
     * database access should be needed.
     * 
     * @param path The file path to read data
     * @param fh The open file handle of the file to read
     * @param buf The ByteBuffer to read data into
     * @param offset The offset into the file to start reading at
     */
    public synchronized void read(String path, long fh, ByteBuffer buf, long offset)
            throws FuseException {
        
        if (log.isTraceEnabled()) {
            log.trace("read - path: " + path + ", fh: " + fh + ", buf: " + buf
                    + ", offset: " + offset);
        }
        
        // the file should be open
        if (!openFiles.containsKey(fh)) {
            FuseException fe = new FuseException("Bad file handle").initErrno(FuseException.EBADF);
            if (log.isDebugEnabled()) {
                log.debug("Bad file handle", fe);
            }
            throw fe;
        }
        
        byte[] bytes = dao.getFileBytes(fh, offset, buf.capacity());
        if (bytes == null) {
            FuseException fe = new FuseException("Bad file handle").initErrno(FuseException.EBADF);
            if (log.isDebugEnabled()) {
                log.debug("Bad file handle", fe);
            }
            throw fe;
        } else if (bytes.length > 0) {
            
        	// return the bytes from the file
        	buf.put(bytes);
            
            // update the file attributes
            FifsFile file = openFiles.get(fh);
            file.getAttributes().atime = (int)(System.currentTimeMillis()/1000L);
            if (!dao.updateFileAttributes(file)) {
            	FuseException fe = new FuseException("I/O Error").initErrno(FuseException.EIO);
                if (log.isDebugEnabled()) {
                    log.debug("I/O Error", fe);
                }
                throw fe;
            }
        }
    }

    /**
     * Write the file contents. 
     * 
     * @param path The path of the file to write
     * @param fh The filehandle of the opened file to write
     * @param isWritePage indicates that the write was caused
     * by a writepage (not sure what that means...)
     * @param buf The bytes to write
     * @param offset The offset within the file to start writing at
     */
    public synchronized void write(String path, long fh, boolean isWritePage, 
            ByteBuffer buf, long offset) throws FuseException {
        
        if (log.isTraceEnabled()) {
            log.trace("write - path: " + path + ", fh: " + fh
                    + ", isWritePage: " + isWritePage + ", buf: " + buf
                    + ", offset: " + offset);
        }
        
        // the file should be open
        if (!openFiles.containsKey(fh)) {
            FuseException fe = new FuseException("Bad file handle").initErrno(FuseException.EBADF);
            if (log.isDebugEnabled()) {
                log.debug("Bad file handle", fe);
            }
            throw fe;
        }

        byte[] bytes = new byte[buf.capacity()];
        buf.get(bytes);
        if (!dao.writeFileBytes(fh, offset, bytes)) {
            FuseException fe = new FuseException("I/O Error").initErrno(FuseException.EIO);
            if (log.isDebugEnabled()) {
                log.debug("I/O Error", fe);
            }
            throw fe;
        } else {
        	// update the file attributes
        	FifsFile file = openFiles.get(fh);
        	FuseStat stat = file.getAttributes();
            stat.atime = stat.mtime = (int)(System.currentTimeMillis()/1000L);
            stat.size += buf.capacity();
            stat.blocks = (int) (stat.size + FIFS_BLOCK_SIZE - 1) / FIFS_BLOCK_SIZE;
            if (!dao.updateFileAttributes(file)) {
                FuseException fe = new FuseException("I/O Error").initErrno(FuseException.EIO);
                if (log.isDebugEnabled()) {
                    log.debug("I/O Error", fe);
                }
                throw fe;
            }
            file.setDirty(true);
        }
    }

    /**
     * Persist the current file contents to the database.
     */
    public synchronized void flush(String path, long fh) throws FuseException {
        if (log.isTraceEnabled()) {
            log.trace("flush - path: " + path + ", fh: " + fh);
        }
    }

    /**
     * Synchronize the file contents. Not sure if we need to do this given the
     * flush hook...
     */
    public synchronized void fsync(String path, long fh, boolean isDataSync)
            throws FuseException {
        if (log.isTraceEnabled()) {
            log.trace("fsync - path: " + path + ", fh: " + fh
                    + ", isDataSync: " + isDataSync);
        }
    }

    /**
     * Create a hard link between the 2 given paths (can be files or directories)
     * Links are unsupported initially
     */
    public synchronized void link(String from, String to) throws FuseException {
        
        if (log.isTraceEnabled()) {
            log.trace("link - from: " + from + ", to: " + to);
        }
        
        FuseException fe = new FuseException("Not implemented").initErrno(FuseException.ENOSYS);
        if (log.isDebugEnabled()) {
            log.debug("Not implemented", fe);
        }
        throw fe;
    }

    /**
     * Create a symbolic link
     * Links are unsupported initially
     */
    public synchronized void symlink(String from, String to) throws FuseException {
        
        if (log.isTraceEnabled()) {
            log.trace("symlink - from: " + from + ", to: " + to);
        }
        
        FuseException fe = new FuseException("Not implemented").initErrno(FuseException.ENOSYS);
        if (log.isDebugEnabled()) {
            log.debug("Not implemented", fe);
        }
        throw fe;
    }

    /**
     * I think this method is supposed to return the source path for a link.
     * Links are initally unsupported.
     */
    public synchronized String readlink(String path) throws FuseException {
        
        if (log.isTraceEnabled()) {
            log.trace("readlink - path: " + path);
        }
        
        FuseException fe = new FuseException("Not implemented").initErrno(FuseException.ENOSYS);
        if (log.isDebugEnabled()) {
            log.debug("Not implemented", fe);
        }
        throw fe;
    }

    
    // ----------------------------------- Main ------------------------------//

    /**
     * Mainline for filesystem in user space
     * 
     * @param args
     *            Command line arguments, must include mount point
     */
    public static void main(String[] args) {

        // first thing, initialize the logging subsystem
        PropertyConfigurator.configure(CONFIG_FILE);

        // must specify mount point on command line
        if (args.length < 3) {
            log.error("Must at least specify mount point on command line");
            System.exit(-1);
        }

        log.info("Starting fifs");

        for (int i = 0; i < args.length; i++) {
            log.trace("arg[" + i + "] = \"" + args[i] + "\"");
        }
        
        try {

            Fifs fifs = new Fifs();

            // load the properties from the config file
            Properties props = new Properties();
            props.load(new FileInputStream(CONFIG_FILE));
            fifs.setProperties(props);

            // create an implementation of the dao
            FifsDaoImpl daoImpl = new FifsDaoImpl();
            daoImpl.init(props);
            fifs.setDao(daoImpl);

            // set the mount point (the last command line argument)
            fifs.setMountPoint(args[args.length-1]);

            // mount the filesystem
            FuseMount.mount(args, fifs);

        } catch (Exception e) {
            log.error("Error with fifs: ", e);
        } finally {
            log.info("Exiting fifs");
        }
    }

}
